﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Threading;
using System.Xml;
using System.Xml.Linq;
//using TetrisService.App_Data;

using System.Security.Cryptography.Xml;
using System.Security.Cryptography;
using System.Text;
using System.IO;
using System.Xml.Serialization;
using System.Data;

namespace TetrisService
{
    /// <summary>
    /// This is the web service facade of a Tetris game.
    /// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
    // To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line. 
    //[System.Web.Script.Services.ScriptService]
    public class TetrisWebService : WebService
    {
        private static readonly int NUM_ROWS = 18;
        private static readonly int NUM_COLUMNS = 10;
        private static readonly int NUM_ROWS_NEXT_TETROMINO = 4;
        private static readonly int NUM_COLUMNS_NEXT_TETROMINO = 5;
        private static readonly int NUM_TETROMINO = 7;
        private static readonly string CURRENT_PIECE = "current_piece";
        private static readonly string BOARD = "board";
        private static readonly string GAME_STATUS = "game_status";
        private static readonly string NEXT_TETROMINO = "next_tetromino";
        private static readonly string USERNAME = "username";

        private static TetrisDataBase dataBase;

        /// <summary>
        /// Initializes a new instance of the <see cref="TetrisWebService"/> class.
        /// </summary>
        public TetrisWebService() 
        {
            initDataBase();
        }

        /// <summary>
        /// Initializes the data base.
        /// </summary>
        private void initDataBase() {
            if(dataBase == null) {
                dataBase = new TetrisDataBase(Server.MapPath("App_Data/database.xml"), "password");
            }
        }

        /// <summary>
        /// Sets the user name to the provided userName.
        /// </summary>
        /// <param name="userName">Name of the user.</param>
        private void setUser(string userName)
        {
            Session[USERNAME] = userName;
        }

        /// <summary>
        /// Returns the user name.
        /// </summary>
        /// <returns></returns>
        private string userName()
        {
            return Session[USERNAME] as string;
        }

        /// <summary>
        /// Returns the game board.
        /// </summary>
        /// <returns></returns>
        private int[][] board()
        {
            if (Session[BOARD] == null)
            {
                Session[BOARD] = createBoard(NUM_ROWS, NUM_COLUMNS);
            }

            return Session[BOARD] as int[][];
        }

        /// <summary>
        /// Generates the next tetromino.
        /// </summary>
        private void generateNextTetromino()
        {
            Random random = new Random();
            Session[NEXT_TETROMINO] = random.Next(NUM_TETROMINO);
        }

        /// <summary>
        /// Returns an integer representing the type of the next tetromino.
        /// </summary>
        /// <returns></returns>
        private int nextTetromino()
        {
            if (Session[NEXT_TETROMINO] == null)
            {
                generateNextTetromino();
            }
            return (int)Session[NEXT_TETROMINO];
        }

        /// <summary>
        /// Returns a new tetromino of the type provided.
        /// </summary>
        /// <param name="tetrominoNumber">The tetromino number.</param>
        /// <param name="board">The board.</param>
        /// <returns></returns>
        private Tetromino getTetromino(int tetrominoNumber, int[][] board)
        {
            switch (tetrominoNumber)
            {
                case 0:
                    return new TetrominoI(board);
                case 1:
                    return new TetrominoJ(board);
                case 2:
                    return new TetrominoL(board);
                case 3:
                    return new TetrominoO(board);
                case 4:
                    return new TetrominoS(board);
                case 5:
                    return new TetrominoT(board);
                default:
                    return new TetrominoZ(board);
            }
        }

        /// <summary>
        /// Returns the next Tetromino
        /// </summary>
        /// <param name="board">The board.</param>
        /// <returns></returns>
        private Tetromino nextTetromino(int[][] board)
        {
            Tetromino next = getTetromino(nextTetromino(), board);
            generateNextTetromino();
            return next;
        }

        /// <summary>
        /// Creates a new game board.
        /// </summary>
        /// <param name="numRows">The num rows.</param>
        /// <param name="numColumns">The num columns.</param>
        /// <returns></returns>
        private int[][] createBoard(int numRows, int numColumns)
        {
            int[][] board = new int[numRows][];
            for (int row = 0; row < numRows; row++)
            {
                board[row] = new int[numColumns];
            }
            return board;
        }

        /// <summary>
        /// Moves all lines one position down.
        /// </summary>
        /// <param name="board">The board.</param>
        /// <param name="row">The row.</param>
        private void moveAllLinesBellow(int[][] board, int row)
        {
            for (int lineToMove = row - 1; lineToMove >= 0; lineToMove--)
            {
                for (int pieceToCopy = 0; pieceToCopy < board[lineToMove].Length; pieceToCopy++)
                {
                    board[lineToMove + 1][pieceToCopy] = 0;
                    if (board[lineToMove][pieceToCopy] > 0)
                    {
                        board[lineToMove + 1][pieceToCopy] = board[lineToMove][pieceToCopy];
                    }
                }
            }
        }

        /// <summary>
        /// Verify if there is a completed line. If there is, the score is updated
        /// </summary>
        /// <param name="board">The game board.</param>
        private void verifyPoints(int[][] board)
        {
            int numLinesCompleted = 0;
            for (int i = 0; i < board.Length; i++)
            {
                if (verifyLineIsComplete(board[i]))
                {
                    moveAllLinesBellow(board, i);
                    numLinesCompleted++;
                }
            }
            addPointToScore(calculatePoints(numLinesCompleted));
            currentGameStatus().addLines(numLinesCompleted);
        }

        /// <summary>
        /// Calculates the points based on the number of completed lines.
        /// </summary>
        /// <param name="numLinesCompleted">The num lines completed.</param>
        /// <returns></returns>
        private int calculatePoints(int numLinesCompleted)
        {
            int value = 0;
            switch (numLinesCompleted)
            {
                case 0:
                    return 0;
                case 1:
                    value = 40;
                    break;
                case 2:
                    value = 100;
                    break;
                case 3:
                    value = 300;
                    break;
                case 4:
                    value = 1200;
                    break;
                default:
                    value = 5000; // This case never happens
                    break;

            }

            return (value * (currentGameStatus().getLevel() + 1));
        }

        /// <summary>
        /// Verifies if the specified line is completed (filled).
        /// </summary>
        /// <param name="line">The line.</param>
        /// <returns></returns>
        private bool verifyLineIsComplete(int[] line)
        {
            for (int i = 0; i < line.Length; i++)
            {
                if (line[i] <= 0)
                {
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// Returns the tetromino that is falling down.
        /// </summary>
        /// <returns></returns>
        private Tetromino currentPiece()
        {
            return Session[CURRENT_PIECE] as Tetromino;
        }

        /// <summary>
        /// Sets the current tetromino.
        /// </summary>
        /// <param name="currentPiece">The current piece.</param>
        private void updateCurrentPiece(Tetromino currentPiece)
        {
            Session[CURRENT_PIECE] = currentPiece;
        }

        /// <summary>
        /// Check if the current tetromino has been set.
        /// </summary>
        /// <returns>
        ///  <c>true</c> if the current tetromino has been set; otherwise, <c>false</c>.
        /// </returns>
        private bool existsCurrentTetromino()
        {
            return Session[CURRENT_PIECE] != null;
        }

        /// <summary>
        /// Returns the current game status.
        /// </summary>
        /// <returns></returns>
        private CurrentGameStatus currentGameStatus()
        {
            if (Session[GAME_STATUS] == null)
            {
                Session[GAME_STATUS] = new CurrentGameStatus();
            }
            return Session[GAME_STATUS] as CurrentGameStatus;
        }

        /// <summary>
        /// Returns the current score of the current match.
        /// </summary>
        /// <returns></returns>
        private int score()
        {
            return currentGameStatus().getScore();
        }

        /// <summary>
        /// Adds the point to score.
        /// </summary>
        /// <param name="points">The points.</param>
        private void addPointToScore(int points)
        {
            updateScore(score() + points);
        }

        /// <summary>
        /// Updates the score.
        /// </summary>
        /// <param name="score">The score.</param>
        private void updateScore(int score)
        {
            currentGameStatus().updateScore(score);
        }

        /// <summary>
        /// Updates the score in the data base.
        /// </summary>
        /// <param name="score">The score.</param>
        private void updateScoreDataBase(int score)
        {
            System.Diagnostics.Debug.WriteLine("[updateScoreDataBase] username=" + userName());
            User user = dataBase.getUser(userName());
            if (user != null)
            {
                System.Diagnostics.Debug.WriteLine("[updateScoreDataBase] score=" + score);
                user.addScoreToFiveBest(score);
                dataBase.updateUser(user);
            }
            else
            {
                System.Diagnostics.Debug.WriteLine("[updateScoreDataBase] USER WAS NULL");
            }
        }

        /// <summary>
        /// Moves the current tetromino to the right.
        /// </summary>
        /// <returns></returns>
        [WebMethod(EnableSession = true)]
        public GameStatus MoveRight()
        {
            Monitor.Enter(this);
            try
            {
                if (existsCurrentTetromino())
                {
                    currentPiece().moveRight();
                }
                return Status();
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Moves the current tetromino to the left.
        /// </summary>
        /// <returns></returns>
        [WebMethod(EnableSession = true)]
        public GameStatus MoveLeft()
        {
            Monitor.Enter(this);
            try
            {
                if (existsCurrentTetromino())
                {
                    currentPiece().moveLeft();
                }
                return Status();
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Moves down the current tetromino.
        /// </summary>
        /// <returns></returns>
        [WebMethod(EnableSession = true)]
        public GameStatus MoveDown() 
        {
            Monitor.Enter(this);
            try
            {
                if (!existsCurrentTetromino() || !currentPiece().moveDown())
                {
                    updateCurrentPiece(nextTetromino(board()));
                }
                verifyPoints(board());
                return Status();
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Rotates the current tetromino.
        /// </summary>
        /// <returns></returns>
        [WebMethod(EnableSession = true)]
        public GameStatus Rotate()
        {
            Monitor.Enter(this);
            try
            {
                if (existsCurrentTetromino())
                {
                    currentPiece().rotate();
                }
                return Status();
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Gets the board.
        /// </summary>
        /// <returns></returns>
        [WebMethod(EnableSession = true)]
        public int[][] GetBoard()
        {
            Monitor.Enter(this);
            try
            {
                return board();
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Adjusts the tetromino blocks so it can be drawn on the next tetromino space.
        /// </summary>
        /// <param name="blocks">The blocks.</param>
        /// <param name="color">The color.</param>
        /// <returns></returns>
        private int[][] adjustTetrominoBlocks(TetrominoBlock[] blocks, int color)
        {
            int[][] nextTetrominoBoard = createBoard(NUM_ROWS_NEXT_TETROMINO, NUM_COLUMNS_NEXT_TETROMINO);
            for (int i = 0; i < blocks.Length; i++)
            {
                nextTetrominoBoard[blocks[i].getY() + 3][blocks[i].getX() - 3] = color;
            }
            return nextTetrominoBoard;
        }

        /// <summary>
        /// Gets the next tetromino.
        /// </summary>
        /// <returns></returns>
        [WebMethod(EnableSession = true)]
        public int[][] GetNextTetromino()
        {
            Monitor.Enter(this);
            try
            {
                Tetromino next = getTetromino(nextTetromino(), null);
                TetrominoBlock[] blocks = next.getTetromino();
                return adjustTetrominoBlocks(blocks, next.color());
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Resets the game.
        /// </summary>
        private void resetGame()
        {
            Session[CURRENT_PIECE] = null;
            Session[GAME_STATUS] = null;
            Session[NEXT_TETROMINO] = null;
            Session[BOARD] = null;
            Session[USERNAME] = null;
        }

        /// <summary>
        /// Gets the game configuration.
        /// </summary>
        /// <param name="userName">Name of the user.</param>
        /// <returns></returns>
        [WebMethod(EnableSession = true)]
        public GameConfiguration GetGameConfiguration(string userName)
        {
            resetGame();
            setUser(userName);
            GameConfiguration gameConfiguration = new GameConfiguration();
            gameConfiguration.numColumns = NUM_COLUMNS;
            gameConfiguration.numRows = NUM_ROWS;
            gameConfiguration.numColumnsNextTetromino = NUM_COLUMNS_NEXT_TETROMINO;
            gameConfiguration.numRowsNextTetromino = NUM_ROWS_NEXT_TETROMINO;
            gameConfiguration.refreshTime = currentGameStatus().getRefreshTime();
            gameConfiguration.bestScores = dataBase.fiveBestScores();
            gameConfiguration.myBestScores = dataBase.getUser(userName).getValidScores();

            return gameConfiguration;
        }

        /// <summary>
        /// Determines whether the game is over.
        /// </summary>
        /// <returns>
        ///   <c>true</c> if the game has come to an end; otherwise, <c>false</c>.
        /// </returns>
        private bool isGameOver()
        {
            for(int i = 4; i < 6; i++) {
                if(board()[0][i] > 0) {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Retruns the status of the game.
        /// </summary>
        /// <returns></returns>
        [WebMethod(EnableSession = true)]
        public GameStatus Status()
        {
            Monitor.Enter(this);
            try
            {
                GameStatus status = new GameStatus();
                status.score = currentGameStatus().getScore();
                status.level = currentGameStatus().getLevel();
                status.refreshTime = currentGameStatus().getRefreshTime();
                status.nextTetromino = nextTetromino();
                status.lines = currentGameStatus().getLines();
                status.isGameOver = isGameOver();
                status.board = board();
                return status;
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Tries to login with the specified user name and password.
        /// </summary>
        /// <param name="userName">Name of the user.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        [WebMethod(EnableSession = true)]
        public bool Login(string userName, string password)
        {
            Monitor.Enter(this);
            try
            {
                User user = dataBase.getUser(userName);
                if (user == null)
                {
                    return false;
                }
                if (user.checkPassword(password))
                {
                    setUser(userName);
                    return true;
                }
                
                return false;
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Creates a new user with the provided user name and password.
        /// </summary>
        /// <param name="userName">Name of the user.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        [WebMethod(EnableSession = true)]
        public bool SignUp(string userName, string password)
        {
            Monitor.Enter(this);
            try
            {
                return dataBase.addUser(userName, password);
            } catch(Exception e) {
                return false;
            }
            finally
            {
                Monitor.Exit(this);
            }
        }

        /// <summary>
        /// Ends the game.
        /// </summary>
        [WebMethod(EnableSession = true)]
        public void FinishGame()
        {
            updateScoreDataBase(currentGameStatus().getScore());
            setUser(null);
        }
    }
}
